/*
	See COPYRIGHT.txt in this directory for full copyright text.
	
	Pretty resumable parser
	Inspired by Google Code Prettify
	Which was ported by Anirudh Sasikumar to AS3

   	Modified and simplified to be able to handle on-the-fly changes,
	by parsing one line at a time, which can be spread out over multiple frames
	as to emulate threading in a Flash runtime.
		
*/

package com.moonshineproject.parser
{
	import flash.events.EventDispatcher;

	public class PrettyResumableParser extends EventDispatcher implements ILineParser
	{
		public static const AS_CODE:int = 1;
		public static const AS_STRING1:int = 2;
		public static const AS_STRING2:int = 3;
		public static const AS_COMMENT:int = 4;
		public static const AS_MULTILINE_COMMENT:int = 5;
		public static const AS_REGULAR_EXPRESSION:int = 6;
		public static const AS_KEYWORD:int = 10;
		public static const AS_VAR_KEYWORD:int = 11;
		public static const AS_FUNCTION_KEYWORD:int = 12;
		public static const AS_PACKAGE_CLASS_KEYWORDS:int = 13;
		
		protected var wordBoundaries:RegExp = /([\s,(){}\[\]\-+*%\/="'~!&|<>?:;.]+)/g;
		
		// TODO: Add patterns for multiline strings
		protected var patterns:Array = [
			[AS_STRING1, 			/^\"(?:\\\\|\\\"|[^\n])*?(?:\"|\\\n|(?=\n))/		], //" 
			[AS_STRING2, 			/^\'(?:\\\\|\\\'|[^\n])*?(?:\'|\\\n|(?=\n))/		],
			[AS_COMMENT, 			/^\/\/.*/									],
			[AS_MULTILINE_COMMENT, 	/^\/\*.*?(?:\*\/|\n)/						],
			[AS_REGULAR_EXPRESSION,	/^\/(?:\\\\|\\\/|\[(?:\\\\|\\\]|.)+?\]|[^*\/])(?:\\\\|\\\/|\[(?:\\\\|\\\]|.)+?\]|.)*?\/[gismx]*/	]
		];
		
		protected var endPatterns:Array = [
			[AS_STRING1,			/(?:^|[^\\])(\"|(?=\n))/					],
			[AS_STRING2,			/(?:^|[^\\])(\'|(?=\n))/					],
			[AS_MULTILINE_COMMENT,	/\*\//										]
		];
		
		protected var keywords:Array = [
			[AS_KEYWORD,
				['is', 'if', 'in', 'as', 'new', 'for', 'use', 'set', 'get', 'try', 
				'null', 'true', 'void', 'else', 'each', 'case', 'this', 'false', 
				'const', 'catch', 'class', 'return', 'switch', 'static', 
				'import', 'private', 'public', 'extends', 'override', 'inherits', 
				'internal', 'implements', 'package', 'protected', 'namespace']
			],
			[AS_VAR_KEYWORD, ['var']],
			[AS_FUNCTION_KEYWORD, ['function']],
			[AS_PACKAGE_CLASS_KEYWORDS, ['package', 'class']]
		];
				
		protected var keywordSet:Object = {};
								
		protected var context:int = AS_CODE;
		protected var result:Vector.<int>;

		public function PrettyResumableParser():void
		{
			super();
			
			for (var i:int = 0; i < keywords.length; i++) 
			{
				var k:Array = keywords[i];
				for (var j:int = 0; j < k[1].length; j++)
				{ 
					keywordSet[ k[1][j] ] = k[0];
				}
			}
			
		}

		public function setContext(newContext:int):void
		{
			context = newContext; 
		}

		public function parse(sourceCode:String):Vector.<int>
		{
			result = new Vector.<int>();
			
			for (var i:int = 0; i < endPatterns.length; i++)
			{
				if (endPatterns[i][0] == context)
				{
					result.push(0, context);
					findContextEnd(sourceCode, endPatterns[i][1]);
					
					break;
				}
			}
			
			if (result.length == 0)
			{
				splitOnContext(sourceCode.toString());
			}
			
			context = result[result.length-1];
			
			return result;
		}
		
		public function isMultilineContext(context:int):Boolean
		{
			for (var i:int = 0; i < endPatterns.length; i++)
			{
				if (endPatterns[i][0] == context) return true;
			}
			
			return false;
		}
		
		protected function findContextEnd(source:String, endPattern:RegExp):void
		{
			var endMatch:Object = endPattern.exec(source);
			
			if (endMatch)
			{
				var matchLen:int = endMatch[0].length;
				
				splitOnContext(source.substring(endMatch.index + matchLen), endMatch.index + matchLen);
			}
		}
		
		/*
			Takes string of source code, assigns styles to this.result.
			Dives instantly when pattern is found, unlike Prettify,
			which nests decoration/result array & then runs over it again.
		*/
		protected function splitOnContext(tail:String, pos:int=0):void
		{
			var style:int = 0;
			
			var lastStyle:int = 0;
			var head:String = "";
			
			// NOTE: for longer strings this could be a for loop & could break & be returned to,
			// as to make the parsing fully psuedo-threaded.
			while (tail.length)
			{
				var match:Array;
				var token:int = 0;
				
				for (var i:int = 0; i < patterns.length; i++)
				{
					match = tail.match(patterns[i][1]);
					if (match)
					{
						token = match[0].length;
						lastStyle = style;
						style = patterns[i][0];
					}
				}
				if (token == 0)
				{
					token = 1;
					head += tail.charAt(0);
					lastStyle = style;
					style = AS_CODE;
				} 
				else if (style != lastStyle && lastStyle == AS_CODE)
				{
					// Decorations are set to this.result instantly by this function
					splitOnKeywords(head, pos-head.length); 
					head = "";
				}
				
				if (style != lastStyle && !head.length)
				{ 
					result.push(pos, style);
				}
				
				pos += token;
				tail = tail.substring(token);
			}
			
			// If head exists it means last matched token was unknown (AS_CODE),
			// so we see if it contains keywords. 
			if (head.length) {
				splitOnKeywords(head, pos-head.length);
			}
		}
		
			
		
		protected function splitOnKeywords(source:String, pos:int):void
		{
			var m:Array = source.split(wordBoundaries);
			var s:String;
			var style:int;
			var lastStyle:int;
			for (var i:int = 0; i < m.length; i++) {
				s = m[i];
				lastStyle = style;
				if (keywordSet.hasOwnProperty(s))
				{
					style = keywordSet[s];
				}
				else if (!/^\s+$/.test(s)) { // Avoid switching styles for whitespace
					style = AS_CODE;
				}
				
				if (style != lastStyle) {
					result.push(pos, style);
				}
				pos += s.length;
			}
		}

	}
}